package dijjer.updater;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import dijjer.AbstractFactory;
import dijjer.util.logging.Logger;

public class Loader extends ClassLoader implements NonUpdateable {

	public static final String VERSION = "$Id: Loader.java 39 2009-04-03 16:49:28Z mathias.demare $";

	public static final String MANUAL = "MANUAL";
	public static final String NEVER = "NEVER";
	public static final String NEW = "NEW";

	public static int UPDATE_FIRST_GOOD_BUILD = 82;	
	private static final String UPDATER_PACKAGE_PREFIX = "dijjer.updater.";
	private static final String BUILD_CLASS = "dijjer.Dijjer";

	private Map _classCache = new HashMap();
	private JarFile _jar;
	private int _loadedBuild = -1;
	private static Loader _loader;
	private static String[] _args = new String[0];

	private static String _updateStrategy = NEW;
	private static final String MAIN_CLASS = "dijjer.Main";

	public static void load(String[] args) throws Exception {
		File base = AbstractFactory.getFactory().getResourceManager().getRuntimeBase();
		JarFile jar = null;
		if (base != null) {
			 jar = new JarFile(base, true);
		}
		Loader loader = new Loader(jar);
		_args = args;
		startLoader(loader);
	}
	
	public static AbstractFactory loadPlugin(String arg) throws Exception {
		String path = (new File(".")).getCanonicalPath() + "/plugins/" + arg;
		URL url = new URL("jar:file://" + path + "!/");
		URLClassLoader ucl = new URLClassLoader(new URL[] { url } );
		Class clas = ucl.loadClass("dijjer.Factory");
		Constructor con = clas.getConstructor(new Class[]{});
		AbstractFactory factory = (AbstractFactory) con.newInstance(new Object[]{});
		return factory;
	}

	private static void startLoader(Loader newLoader) throws Exception {
		try {
			if (_loader != null) {
				Logger.info("Uninstall loader : " + _loader);
				_loader.shutdown(Boolean.FALSE);
			}
			if (newLoader != null) {
				_loader = newLoader;
			} else {
				_loader = load();
			}
			Logger.info("Installed new loader : " + _loader);
			_loader.start();
		} catch (Exception ex) {
			ex.printStackTrace();
			System.exit(301);
		}
	}

	private void shutdown(Boolean exit) throws Exception {
		Class mainClass = _loader.loadClass(MAIN_CLASS);
		Method mainMethod = mainClass.getMethod("shutdown", new Class[]{Boolean.TYPE});
		mainMethod.invoke(null, new Object[]{exit});
	}

	private void start() throws Exception {
		Class mainClass = _loader.loadClass(MAIN_CLASS);
		Method mainMethod = mainClass.getMethod("start", new Class[]{new String[0].getClass()});
		AbstractFactory.getFactory().getResourceManager().revive();		
		mainMethod.invoke(null, new Object[]{_args});
	}

	public static void stopLoader() throws Exception {
		if (_loader != null) {
			Logger.info("Uninstall loader and shutdown : " + _loader);
			_loader.shutdown(Boolean.TRUE);
		}
	}

	public static void reload(int requiredMinimumBuild, boolean manual) throws Exception {
		if (neverUpdate()) {
			Logger.warning("Automatic reloads have been disabled.");
			return;
		}

		if ((_loader != null) && (_loader.getLoadedBuild() < requiredMinimumBuild)) {
			if (!manual && manualUpdate()) {
				Logger.warning("Manual reload required (http://localhost:port/update");
				return;
			}
			AbstractFactory.getFactory().getResourceManager().downloadCodeBase();
			Loader loader = load();
			if (loader.getLoadedBuild() >= requiredMinimumBuild) {
				startLoader(loader);
			} else {
				Logger.warning("Cannot find newer build at download site: " + AbstractFactory.getFactory().getResourceManager().getDownloadURL());
			}
			AbstractResourceManager.cleanupCache();
		}
	}

	private int getLoadedBuild() {
		return _loadedBuild;
	}

	public Loader(JarFile jar) throws Exception {
		_jar = jar;
		loadClass(BUILD_CLASS);
	}

	private static Loader load() throws Exception {
		JarFile jar = new JarFile(AbstractFactory.getFactory().getResourceManager().getRuntimeBase(), true);
		return new Loader(jar);
	}

	public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
		boolean fromSystem = false;
		Class clas = null;

		// Don't allow auto-updating of some core pieces
		if (_jar == null || name.startsWith(UPDATER_PACKAGE_PREFIX) || neverUpdate()) {
			Logger.debug(this + " Updater from system = " + name);
			clas = findSystemClass(name);
			fromSystem = true;
		}

		if (!fromSystem) {

			if (clas == null && _classCache.containsKey(name)) {
				Logger.debug("Class from cache = " + name);
				clas = (Class) _classCache.get(name);
			}


			if (clas == null) {
				String classFilename = name.replace('.', '/') + ".class";
				try {
					byte raw[] = getBytes(classFilename);
					clas = defineClass(name, raw, 0, raw.length);
					Logger.debug("Class from jar = " + name);
				} catch (Exception ie) {
				}
			}

			if (clas != null && NonUpdateable.class.isAssignableFrom(clas)) {
				Logger.debug("NonUpdateable Class = " + name);
				Class c = findSystemClass(name);
				if (c != null) {
					clas = c;
				}
			}

			if (clas == null) {
				Logger.debug("Class from system = " + name);
				clas = findSystemClass(name);
			}

			if (clas != null) {
				_classCache.put(name, clas);
			}
		}

		if (resolve && clas != null) {
			resolveClass(clas);
		}

		if (clas == null) {
			throw new ClassNotFoundException(name);
		}

		if (BUILD_CLASS.equals(name)) {
			try {
				_loadedBuild = ((Integer) clas.getField("BUILD").get(null)).intValue();
				Logger.debug(this + " : Got build = " + _loadedBuild);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		return clas;
	}

	protected Class findClass(String name) throws ClassNotFoundException {
		System.err.println("**** This shouldn't be used, I think!!!  = " + name);
		return findClass(name);
	}

	public Class loadClass(String name) throws ClassNotFoundException {
		return loadClass(name, false);
	}

	private byte[] getBytes(String filename) throws IOException {
		JarEntry entry = _jar.getJarEntry(filename);
		final int classSize = (int) entry.getSize();

		BufferedInputStream fin = new BufferedInputStream(_jar.getInputStream(entry));


		byte rawBytes[] = new byte[classSize];
		int r = fin.read(rawBytes);
		if (r != classSize) {
			throw new IOException("Can't read all, " + r + " != " + entry.getSize());
		}
		fin.close();

		return rawBytes;
	}

	public static void setUpdateStrategy(String updateStrategy) {
		_updateStrategy = updateStrategy;
	}

	private static boolean manualUpdate() {
		return MANUAL.equalsIgnoreCase(_updateStrategy);
	}

	private static boolean neverUpdate() {
		return NEVER.equalsIgnoreCase(_updateStrategy);
	}

	public String toString() {
		return "Loader{" +
		    "jar = " + (_jar == null ? "Bootstrap" : _jar.getName()) +
		    ", Build = " + _loadedBuild +
		    "}";
	}
}
